// Claude-authored implementation of RFC 6570 URI Templates

export type Variables = Record<string, string | string[]>;

const MAX_TEMPLATE_LENGTH = 1000000; // 1MB
const MAX_VARIABLE_LENGTH = 1000000; // 1MB
const MAX_TEMPLATE_EXPRESSIONS = 10000;
const MAX_REGEX_LENGTH = 1000000; // 1MB

export class UriTemplate {
    /**
     * Returns true if the given string contains any URI template expressions.
     * A template expression is a sequence of characters enclosed in curly braces,
     * like {foo} or {?bar}.
     */
    static isTemplate(str: string): boolean {
        // Look for any sequence of characters between curly braces
        // that isn't just whitespace
        return /\{[^}\s]+\}/.test(str);
    }

    private static validateLength(str: string, max: number, context: string): void {
        if (str.length > max) {
            throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`);
        }
    }
    private readonly template: string;
    private readonly parts: Array<string | { name: string; operator: string; names: string[]; exploded: boolean }>;

    get variableNames(): string[] {
        return this.parts.flatMap(part => (typeof part === 'string' ? [] : part.names));
    }

    constructor(template: string) {
        UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, 'Template');
        this.template = template;
        this.parts = this.parse(template);
    }

    toString(): string {
        return this.template;
    }

    private parse(template: string): Array<string | { name: string; operator: string; names: string[]; exploded: boolean }> {
        const parts: Array<string | { name: string; operator: string; names: string[]; exploded: boolean }> = [];
        let currentText = '';
        let i = 0;
        let expressionCount = 0;

        while (i < template.length) {
            if (template[i] === '{') {
                if (currentText) {
                    parts.push(currentText);
                    currentText = '';
                }
                const end = template.indexOf('}', i);
                if (end === -1) throw new Error('Unclosed template expression');

                expressionCount++;
                if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) {
                    throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`);
                }

                const expr = template.slice(i + 1, end);
                const operator = this.getOperator(expr);
                const exploded = expr.includes('*');
                const names = this.getNames(expr);
                const name = names[0];

                // Validate variable name length
                for (const name of names) {
                    UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
                }

                parts.push({ name, operator, names, exploded });
                i = end + 1;
            } else {
                currentText += template[i];
                i++;
            }
        }

        if (currentText) {
            parts.push(currentText);
        }

        return parts;
    }

    private getOperator(expr: string): string {
        const operators = ['+', '#', '.', '/', '?', '&'];
        return operators.find(op => expr.startsWith(op)) || '';
    }

    private getNames(expr: string): string[] {
        const operator = this.getOperator(expr);
        return expr
            .slice(operator.length)
            .split(',')
            .map(name => name.replace('*', '').trim())
            .filter(name => name.length > 0);
    }

    private encodeValue(value: string, operator: string): string {
        UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, 'Variable value');
        if (operator === '+' || operator === '#') {
            return encodeURI(value);
        }
        return encodeURIComponent(value);
    }

    private expandPart(
        part: {
            name: string;
            operator: string;
            names: string[];
            exploded: boolean;
        },
        variables: Variables
    ): string {
        if (part.operator === '?' || part.operator === '&') {
            const pairs = part.names
                .map(name => {
                    const value = variables[name];
                    if (value === undefined) return '';
                    const encoded = Array.isArray(value)
                        ? value.map(v => this.encodeValue(v, part.operator)).join(',')
                        : this.encodeValue(value.toString(), part.operator);
                    return `${name}=${encoded}`;
                })
                .filter(pair => pair.length > 0);

            if (pairs.length === 0) return '';
            const separator = part.operator === '?' ? '?' : '&';
            return separator + pairs.join('&');
        }

        if (part.names.length > 1) {
            const values = part.names.map(name => variables[name]).filter(v => v !== undefined);
            if (values.length === 0) return '';
            return values.map(v => (Array.isArray(v) ? v[0] : v)).join(',');
        }

        const value = variables[part.name];
        if (value === undefined) return '';

        const values = Array.isArray(value) ? value : [value];
        const encoded = values.map(v => this.encodeValue(v, part.operator));

        switch (part.operator) {
            case '':
                return encoded.join(',');
            case '+':
                return encoded.join(',');
            case '#':
                return '#' + encoded.join(',');
            case '.':
                return '.' + encoded.join('.');
            case '/':
                return '/' + encoded.join('/');
            default:
                return encoded.join(',');
        }
    }

    expand(variables: Variables): string {
        let result = '';
        let hasQueryParam = false;

        for (const part of this.parts) {
            if (typeof part === 'string') {
                result += part;
                continue;
            }

            const expanded = this.expandPart(part, variables);
            if (!expanded) continue;

            // Convert ? to & if we already have a query parameter
            if ((part.operator === '?' || part.operator === '&') && hasQueryParam) {
                result += expanded.replace('?', '&');
            } else {
                result += expanded;
            }

            if (part.operator === '?' || part.operator === '&') {
                hasQueryParam = true;
            }
        }

        return result;
    }

    private escapeRegExp(str: string): string {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    private partToRegExp(part: {
        name: string;
        operator: string;
        names: string[];
        exploded: boolean;
    }): Array<{ pattern: string; name: string }> {
        const patterns: Array<{ pattern: string; name: string }> = [];

        // Validate variable name length for matching
        for (const name of part.names) {
            UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
        }

        if (part.operator === '?' || part.operator === '&') {
            for (let i = 0; i < part.names.length; i++) {
                const name = part.names[i];
                const prefix = i === 0 ? '\\' + part.operator : '&';
                patterns.push({
                    pattern: prefix + this.escapeRegExp(name) + '=([^&]+)',
                    name
                });
            }
            return patterns;
        }

        let pattern: string;
        const name = part.name;

        switch (part.operator) {
            case '':
                pattern = part.exploded ? '([^/]+(?:,[^/]+)*)' : '([^/,]+)';
                break;
            case '+':
            case '#':
                pattern = '(.+)';
                break;
            case '.':
                pattern = '\\.([^/,]+)';
                break;
            case '/':
                pattern = '/' + (part.exploded ? '([^/]+(?:,[^/]+)*)' : '([^/,]+)');
                break;
            default:
                pattern = '([^/]+)';
        }

        patterns.push({ pattern, name });
        return patterns;
    }

    match(uri: string): Variables | null {
        UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, 'URI');
        let pattern = '^';
        const names: Array<{ name: string; exploded: boolean }> = [];

        for (const part of this.parts) {
            if (typeof part === 'string') {
                pattern += this.escapeRegExp(part);
            } else {
                const patterns = this.partToRegExp(part);
                for (const { pattern: partPattern, name } of patterns) {
                    pattern += partPattern;
                    names.push({ name, exploded: part.exploded });
                }
            }
        }

        pattern += '$';
        UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern');
        const regex = new RegExp(pattern);
        const match = uri.match(regex);

        if (!match) return null;

        const result: Variables = {};
        for (let i = 0; i < names.length; i++) {
            const { name, exploded } = names[i];
            const value = match[i + 1];
            const cleanName = name.replace('*', '');

            if (exploded && value.includes(',')) {
                result[cleanName] = value.split(',');
            } else {
                result[cleanName] = value;
            }
        }

        return result;
    }
}
